
module ActiveRecord
  class Base
    
    def self.asynchronous(fields)
    
      fields = [fields] if fields.kind_of?(Symbol)
  
      fields.each do |field|
        
        alias_method "synchronous_#{field}".to_sym, field
        
        define_method(field) do |*params|
          
          queued = QueuedMethod.new
          queued.object = self.to_yaml
          queued.called_method = "synchronous_#{field}"
          queued.params = params
                  
          queued.save!
        end
      end
    end
  end
end


class MethodQueue
  
  #Load the object from the database
  def self.load_object(queued_method)
    # have to load twice.  one to get its class name, second to load the actual class up
    object = YAML::load(queued_method.object)
    
    if !object.respond_to?("reload")
      require_or_load object.class.to_s.underscore  
      object = YAML::load(queued_method.object)
    end
    
    #make sure the objects class is loaded
    object.send("reload")  
  end
  
  #invoke a method, record the results, mark it completed and possibly failed.
  #If failed, the results will be the exception object that was thrown.
  def self.invoke_method(queued_method, object)
    results = nil
    
    begin
      queued_method.ran_at = Time.now
      queued_method.save
      # thread = Thread.new(queued_method) {|qm|
      #   #save the ran at time in parallel so it's fairly accurate and we don't have to wait until
      #   #completion to know what it is.
      #   qm.save
      # }
      results = object.send(queued_method.called_method.to_sym, *(queued_method.params) )
      queued_method.failed = false
    rescue => caught
      results = caught
      queued_method.failed = true
    end
    # thread.join
    queued_method.completed_at = Time.now
    queued_method.results = results.to_s
    queued_method.save
    
    return results
  end
  
  #cancel a method. If it's already taken, it may still be processed.
  def self.cancel_method(queued_method)
    queued_method.cancelled_at = Time.now
    queued_method.save
  end

  #invoke all the pending methods in the order they were queued
  #If a block is provided, each pending method and the results will be yielded to it.
  def self.process
    take_pending do |pending_method|
      object = load_object(pending_method)
      results = invoke_method(pending_method, object)
      yield pending_method, results if block_given?
    end
  end
  
  #atomically take all the pending methods and yield them to the calling block
  def self.take_pending
    now = Time.now
    num_modified = QueuedMethod.update_all(["taken_at = ?",now], "taken_at IS NULL and cancelled_at IS NULL")
    queue = QueuedMethod.find(:all, :conditions => {:taken_at => now}, :order => :created_at)
    queue.each do |each_method|
      yield each_method
    end
    return num_modified
  end
  
  #finds all the pending methods and yeild them to the calling block
  def self.each_pending
    pending_methods.each do |each_method|
      yield each_method
    end
  end
  
  def self.pending_methods
    QueuedMethod.find(:all, :conditions => "taken_at IS NULL and cancelled_at IS NULL")
  end
end
